/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.xiaopo.flying.stickerview;

import com.xiaopo.flying.stickerview.util.ResUtil;
import ohos.agp.animation.Animator;
import ohos.agp.animation.AnimatorValue;
import ohos.agp.components.AttrHelper;
import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.render.Canvas;
import ohos.agp.render.Paint;
import ohos.agp.render.PixelMapHolder;
import ohos.agp.utils.Color;
import ohos.agp.utils.RectFloat;
import ohos.app.Context;
import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;
import ohos.eventhandler.InnerEvent;
import ohos.media.image.PixelMap;
import ohos.media.image.common.PixelFormat;
import ohos.media.image.common.Rect;
import ohos.media.image.common.Size;
import ohos.multimodalinput.event.TouchEvent;

import java.io.InputStream;
import java.util.Optional;

/**
 * 自定义PhotoView
 *
 * @since 2021-05-12
 */
public class PhotoView extends Component implements Component.DrawTask, Component.TouchEventListener {
    private static final int TWO = 2;
    private static final int THREE = 3;
    private static final int THIRTY = 30;
    private static final int THREE_HUNDRED = 300;
    private static final int SIX_HUNDRED = 600;
    private static final int DURATION_TIME = 200;
    private static final float TEN = 10f;
    private static final double QUICKSCALE = 5.8;
    private float touchX;
    private float touchY;
    private float touchX2;
    private float touchY2;
    private float ratio = 1;
    private float minRatio = 1;
    private float maxRatio = TEN;
    private float offsetX;
    private float offsetY;
    private float touchPcX;
    private float touchPcY;
    private PixelMap orgPixelMap;
    private PixelMap bgPixelMap;
    private PixelMapHolder bgPixelMapHolder;
    /**
     * 背景图缩放比
     */
    private int bgScale = 1;
    /**
     * 横向切割比
     */
    private int widthScale = 1;
    /**
     * 纵向切割比
     */
    private int heightScale = 1;
    private Paint paint = new Paint();
    private Paint tilePaint = new Paint();
    private AnimatorValue scaleAnimatorValue;
    /**
     * 处理双击事件
     */
    private long firstDown;
    private float firstDownTouchX;
    private float firstDownTouchY;
    private long firstUp;
    private float firstUpTouchX;
    private float firstUpTouchY;
    private long secondDown;
    private float secondDownTouchX;
    private float secondDownTouchY;
    private long secondUp;
    private boolean isScaleAnimator;
    private boolean isLoad;
    private boolean isAnimatorFirst = true;
    private double quickScale = QUICKSCALE;
    private RectFloat rect;
    private RectFloat rectf;
    private EventHandler eventHandler = new EventHandler(EventRunner.getMainEventRunner()) {
        @Override
        protected void processEvent(InnerEvent event) {
            isLoad = false;
            invalidate();
        }
    };

    /**
     * 构造
     *
     * @param context 上下文
     */
    public PhotoView(Context context) {
        this(context, null, null);
    }

    /**
     * 构造
     *
     * @param context 上下文
     * @param attrSet 自定义属性
     */
    public PhotoView(Context context, AttrSet attrSet) {
        this(context, attrSet, null);
    }

    /**
     * 构造
     *
     * @param context 上下文
     * @param attrSet 自定义属性
     * @param styleName 格式名称
     */
    public PhotoView(Context context, AttrSet attrSet, String styleName) {
        super(context, attrSet, styleName);
        init();
    }

    private PixelMap getPixelMapFromResId(int origin) {
        Optional<PixelMap> optional = ResUtil.getPixelMap(getContext(), origin);
        PixelMap pixelMap = optional.isPresent() ? optional.get() : null;
        return pixelMap;
    }

    private PixelMap getPixelMapFromPath(String path) {
        Optional<PixelMap> optional = ResUtil.getPixelMap(getContext(), path);
        PixelMap pixelMap = optional.isPresent() ? optional.get() : null;
        return pixelMap;
    }

    private PixelMap getPixelMapFromUrl(InputStream origin) {
        Optional<PixelMap> optional = ResUtil.getPixelMap(getContext(), origin);
        PixelMap pixelMap = optional.isPresent() ? optional.get() : null;
        return pixelMap;
    }

    /**
     * set PixelMap with resId
     *
     * @param res the pixelMap resourceId
     */
    public void setResPixelMap(int res) {
        isLoad = true;
        if (orgPixelMap != null) {
            orgPixelMap.release();
            orgPixelMap = null;
        }
        new Thread(
            () -> {
                orgPixelMap = getPixelMapFromResId(res);
                initPixelMap();
            })
            .start();
    }

    public PixelMapHolder getOrgPixelMap() {
        return bgPixelMapHolder;
    }

    public RectFloat getRect() {
        return rect;
    }

    public RectFloat getRectf() {
        return rectf;
    }

    /**
     * set PixelMap with url
     *
     * @param url the url
     */
    public void setUrlPixelMap(String url) {
        isLoad = true;
        if (orgPixelMap != null) {
            orgPixelMap.release();
            orgPixelMap = null;
        }
    }

    /**
     * set PixelMap with path
     *
     * @param path the pixelMap path
     */
    public void setFilePath(String path) {
        isLoad = true;
        if (orgPixelMap != null) {
            orgPixelMap.release();
            orgPixelMap = null;
        }
        new Thread(
            () -> {
                orgPixelMap = getPixelMapFromPath(path);
                initPixelMap();
            })
            .start();
    }

    /**
     * set PixelMap
     *
     * @param pixelMap the pixelMap
     */
    public void setPixelMap(PixelMap pixelMap) {
        isLoad = true;
        if (orgPixelMap != null) {
            orgPixelMap.release();
            orgPixelMap = null;
        }
        orgPixelMap = pixelMap;
        initPixelMap();
    }

    private void initPixelMap() {
        new Thread(
            () -> {
                while (getWidth() == 0 || getHeight() == 0) {
                    isLoad = true;
                }
                if (orgPixelMap == null) {
                    return;
                }
                reset();
                minRatio = (float) calculateMinRatio();
                initBgPixelMap();
                eventHandler.sendEvent(0);
            })
            .start();
    }

    private void initBgPixelMap() {
        if (minRatio > 1) {
            bgPixelMap = orgPixelMap;
            bgPixelMapHolder = new PixelMapHolder(bgPixelMap);
        } else {
            double sr = minRatio;
            while (sr < 1) {
                sr = sr * TWO;
                bgScale = bgScale * TWO;
            }
            float wr = getWidth() * 1.0f / orgPixelMap.getImageInfo().size.width;
            while (wr < 1) {
                wr = wr * TWO;
                widthScale = widthScale * TWO;
            }
            float hr = getHeight() * 1.0f / orgPixelMap.getImageInfo().size.height;
            while (hr < 1) {
                hr = hr * TWO;
                heightScale = heightScale * TWO;
            }
            PixelMap.InitializationOptions initializationOptions =
                new PixelMap.InitializationOptions();
            initializationOptions.size =
                new Size(
                    orgPixelMap.getImageInfo().size.width / bgScale,
                    orgPixelMap.getImageInfo().size.height / bgScale);
            initializationOptions.pixelFormat = PixelFormat.ARGB_8888;
            bgPixelMap =
                PixelMap.create(
                    orgPixelMap,
                    new Rect(
                        0,
                        0,
                        orgPixelMap.getImageInfo().size.width,
                        orgPixelMap.getImageInfo().size.height),
                    initializationOptions);
            bgPixelMapHolder = new PixelMapHolder(bgPixelMap);
        }
    }

    private void reset() {
        touchX = 0;
        touchY = 0;
        touchX2 = 0;
        touchY2 = 0;
        ratio = 1;
        minRatio = 0;
        offsetX = 0;
        offsetY = 0;
        touchPcX = 0;
        touchPcY = 0;
        bgScale = 1;
        widthScale = 1;
        heightScale = 1;
    }

    private double calculateMinRatio() {
        if (orgPixelMap != null) {
            return Math.min(getWidth() * 1.0f / orgPixelMap.getImageInfo().size.width,
                getHeight() * 1.0f / orgPixelMap.getImageInfo().size.height);
        } else {
            return minRatio;
        }
    }

    private float getTouchX(TouchEvent touchEvent, int index) {
        float getX = 0;
        if (touchEvent.getPointerCount() > index) {
            int[] xy = getLocationOnScreen();
            if (xy != null && xy.length == TWO) {
                getX = touchEvent.getPointerScreenPosition(index).getX() - xy[0];
            } else {
                getX = touchEvent.getPointerPosition(index).getX();
            }
        }
        return getX;
    }

    private float getTouchY(TouchEvent touchEvent, int index) {
        float getY = 0;
        if (touchEvent.getPointerCount() > index) {
            int[] xy = getLocationOnScreen();
            if (xy != null && xy.length == TWO) {
                getY = touchEvent.getPointerScreenPosition(index).getY() - xy[1];
            } else {
                getY = touchEvent.getPointerPosition(index).getY();
            }
        }
        return getY;
    }

    private void init() {
        paint.setAntiAlias(true);
        paint.setFilterBitmap(true);
        paint.setDither(true);
        setTouchEventListener(this);
        addDrawTask(this);
    }

    @Override
    public boolean onTouchEvent(Component component, TouchEvent touchEvent) {
        if (isScaleAnimator) {
            return true;
        }
        if (touchEvent.getPointerCount() >= TWO) {
            if (touchEvent.getAction() == TouchEvent.OTHER_POINT_DOWN
                || touchEvent.getAction() == TouchEvent.PRIMARY_POINT_DOWN) {
                doTwoTouchDown(touchEvent);
            } else if (touchEvent.getAction() == TouchEvent.POINT_MOVE) {
                doTwoTouchMove(touchEvent);
            } else if (touchEvent.getAction() == TouchEvent.OTHER_POINT_UP
                || touchEvent.getAction() == TouchEvent.PRIMARY_POINT_UP) {
                doTwoTouchUp(touchEvent);
            }
            return true;
        } else if (touchEvent.getPointerCount() == 1) {
            if (touchEvent.getAction() == TouchEvent.PRIMARY_POINT_DOWN) {
                doOneTouchDown(touchEvent);
            } else if (touchEvent.getAction() == TouchEvent.POINT_MOVE) {
                doOneTouchMove(touchEvent);
                invalidate();
                return true;
            } else if (touchEvent.getAction() == TouchEvent.PRIMARY_POINT_UP) {
                doOneTouchUp(touchEvent);
            }
        }
        if (isScaleAnimator) {
            return true;
        }
        return true;
    }

    private void doTwoTouchDown(TouchEvent touchEvent) {
        touchX2 = getTouchX(touchEvent, 1);
        touchY2 = getTouchY(touchEvent, 1);
        touchX = getTouchX(touchEvent, 0);
        touchY = getTouchY(touchEvent, 0);

        touchPcX =
            (float)
                ((((getTouchX(touchEvent, 0) + getTouchX(touchEvent, 1)) / TWO) - offsetX)
                    / ratio);
        touchPcY =
            (float)
                ((((getTouchY(touchEvent, 0) + getTouchY(touchEvent, 1)) / TWO) - offsetY)
                    / ratio);
    }

    private void doTwoTouchMove(TouchEvent touchEvent) {
        double yuanRatio = ratio;
        ratio =
            (float) (ratio
                            * Math.sqrt(
                            (getTouchX(touchEvent, 0) - getTouchX(touchEvent, 1))
                                * (getTouchX(touchEvent, 0)
                                - getTouchX(touchEvent, 1))
                                + (getTouchY(touchEvent, 0) - getTouchY(touchEvent, 1))
                                * (getTouchY(touchEvent, 0)
                                - getTouchY(touchEvent, 1)))
                            / Math.sqrt(
                            (touchX - touchX2) * (touchX - touchX2)
                                + (touchY - touchY2) * (touchY - touchY2)));
        if (ratio > maxRatio) {
            ratio = maxRatio;
        }
        if (ratio < minRatio) {
            ratio = minRatio;
        }

        if (orgPixelMap.getImageInfo().size.width * ratio > getWidth()
            || orgPixelMap.getImageInfo().size.height * ratio > getHeight()) {
            offsetX +=
                (getTouchX(touchEvent, 0) + getTouchX(touchEvent, 1)) / TWO - (touchX + touchX2) / TWO;
            offsetX -= touchPcX * ratio - touchPcX * yuanRatio;

            offsetY +=
                (getTouchY(touchEvent, 0) + getTouchY(touchEvent, 1)) / TWO - (touchY + touchY2) / TWO;
            offsetY -= touchPcY * ratio - touchPcY * yuanRatio;
        } else {
            offsetX -= touchPcX * ratio - touchPcX * yuanRatio;
            offsetY -= touchPcY * ratio - touchPcY * yuanRatio;
        }

        touchX2 = getTouchX(touchEvent, 1);
        touchY2 = getTouchY(touchEvent, 1);
        touchX = getTouchX(touchEvent, 0);
        touchY = getTouchY(touchEvent, 0);
        invalidate();
    }

    private void doTwoTouchUp(TouchEvent touchEvent) {
        if (touchEvent.getIndex() == 0 && touchEvent.getPointerCount() == TWO) {
            touchX = getTouchX(touchEvent, 1);
            touchY = getTouchY(touchEvent, 1);
        }
    }

    private void doOneTouchDown(TouchEvent touchEvent) {
        touchX = getTouchX(touchEvent, 0);
        touchY = getTouchY(touchEvent, 0);
        long thisTime = System.currentTimeMillis();
        if (thisTime - firstDown > SIX_HUNDRED) {
            resetDoubleTouch();
            firstDown = thisTime;
            firstDownTouchX = touchX;
            firstDownTouchY = touchY;
        } else if (secondDown == 0
            && thisTime - firstUp < THREE_HUNDRED
            && Math.abs(touchX - firstUpTouchX) < AttrHelper.fp2px(THIRTY, getContext())
            && Math.abs(touchY - firstUpTouchY) < AttrHelper.fp2px(THIRTY, getContext())) {
            secondDown = thisTime;
            secondDownTouchX = touchX;
            secondDownTouchY = touchY;
        } else {
            resetDoubleTouch();
            firstDown = thisTime;
            firstDownTouchX = touchX;
            firstDownTouchY = touchY;
        }
    }

    private void doOneTouchMove(TouchEvent touchEvent) {
        if (getHeight() < orgPixelMap.getImageInfo().size.height * ratio) {
            offsetY += getTouchY(touchEvent, 0) - touchY;
        }
        if (getWidth() < orgPixelMap.getImageInfo().size.width * ratio) {
            offsetX += getTouchX(touchEvent, 0) - touchX;
        }
        touchY = getTouchY(touchEvent, 0);
        touchX = getTouchX(touchEvent, 0);
    }

    private void doOneTouchUp(TouchEvent touchEvent) {
        touchX = getTouchX(touchEvent, 0);
        touchY = getTouchY(touchEvent, 0);
        long thisTime = System.currentTimeMillis();
        if (firstUp == 0
            && thisTime - firstDown < THREE_HUNDRED
            && Math.abs(touchX - firstDownTouchX) < AttrHelper.fp2px(THIRTY, getContext())
            && Math.abs(touchY - firstDownTouchY) < AttrHelper.fp2px(THIRTY, getContext())) {
            firstUp = thisTime;
            firstUpTouchX = touchX;
            firstUpTouchY = touchY;
        } else if (secondUp == 0
            && thisTime - secondDown < THREE_HUNDRED
            && Math.abs(touchX - secondDownTouchX) < AttrHelper.fp2px(THIRTY, getContext())
            && Math.abs(touchY - secondDownTouchY) < AttrHelper.fp2px(THIRTY, getContext())) {
            secondUp = thisTime;
            if (orgPixelMap != null) {
                calScaleAnimator(touchX, touchY, ratio);
            }
            resetDoubleTouch();
        } else {
            resetDoubleTouch();
        }
    }

    @Override
    public void onDraw(Component component, Canvas canvas) {
        tilePaint.setColor(Color.WHITE);
        canvas.drawRect(new RectFloat(0, 0, getWidth(), getHeight()), tilePaint);
        if (orgPixelMap != null && bgPixelMap != null && !isLoad) {
            if (ratio == 1) {
                ratio = minRatio;
            }
            checkZoom();
            if (!isScaleAnimator && isAnimatorFirst) {
                isAnimatorFirst = false;
                offsetY = (float) (getHeight() - orgPixelMap.getImageInfo().size.height * ratio) / TWO;
            }

            rect = new RectFloat(
                (float) (-offsetX / ratio),
                (float) (-offsetY / ratio),
                (float)
                    (orgPixelMap.getImageInfo().size.width
                        - ((orgPixelMap.getImageInfo().size.width) * ratio
                        + offsetX
                        - getWidth())
                        / ratio),
                (float)
                    (orgPixelMap.getImageInfo().size.height
                        - ((orgPixelMap.getImageInfo().size.height) * ratio
                        + offsetY
                        - getHeight())
                        / ratio));

            calCurrentSam();
            if (bgPixelMap != null) {
                rect =
                    new RectFloat(
                        rect.left / bgScale,
                        rect.top / bgScale,
                        rect.right / bgScale,
                        rect.bottom / bgScale);
                rectf = new RectFloat(0, 0, getWidth() - 0, getHeight() - 0);
                if (bgPixelMapHolder == null) {
                    bgPixelMapHolder = new PixelMapHolder(bgPixelMap);
                }
                canvas.drawPixelMapHolderRect(bgPixelMapHolder, rect, rectf, paint);
            }
        }
    }

    private void resetDoubleTouch() {
        firstDown = 0;
        firstUp = 0;
        secondUp = 0;
        secondDown = 0;
    }

    private void calCurrentSam() {
        int ratioOrg = 1;
        while (ratioOrg * ratio < 1) {
            ratioOrg = ratioOrg * TWO;
        }
    }

    private boolean checkZoom() {
        boolean isMoveToStrock = false;
        int width = getWidth();
        int height = getHeight();
        if (width - orgPixelMap.getImageInfo().size.width * ratio > offsetX) {
            offsetX = (float) (width - orgPixelMap.getImageInfo().size.width * ratio);
            isMoveToStrock = true;
        }
        if (0 < offsetX) {
            offsetX = 0;
            isMoveToStrock = true;
        }
        if (height - orgPixelMap.getImageInfo().size.height * ratio > offsetY) {
            offsetY = (float) (height - orgPixelMap.getImageInfo().size.height * ratio);
            isMoveToStrock = true;
        }
        if (0 < offsetY) {
            offsetY = 0;
            isMoveToStrock = true;
        }
        if (width > orgPixelMap.getImageInfo().size.width * ratio) {
            offsetX = (float) (width - orgPixelMap.getImageInfo().size.width * ratio) / TWO;
            isMoveToStrock = true;
        }
        if (height > orgPixelMap.getImageInfo().size.height * ratio) {
            offsetY = (float) (height - orgPixelMap.getImageInfo().size.height * ratio) / TWO;
            isMoveToStrock = true;
        }
        return isMoveToStrock;
    }

    private void calScaleAnimator(float touchx, float touchy, double mratio) {
        if (mratio == maxRatio) {
            // 复位
            float toX = 0;
            float toY = 0;
            if (getWidth() > orgPixelMap.getImageInfo().size.width * minRatio) {
                toX = (float) (getWidth() - orgPixelMap.getImageInfo().size.width * minRatio) / TWO;
            }
            if (getHeight() > orgPixelMap.getImageInfo().size.height * minRatio) {
                toY = (float) (getHeight() - orgPixelMap.getImageInfo().size.height * minRatio) / TWO;
            }
            startScaleAnimator(mratio, minRatio, offsetX, toX, offsetY, toY);
            quickScale = QUICKSCALE;
        } else {
            // 放大
            float toX = (float) (touchx - (touchx - offsetX) / mratio * quickScale);
            float toY = (float) (touchy - (touchy - offsetY) / mratio * quickScale);

            if (getWidth() - orgPixelMap.getImageInfo().size.width * quickScale > toX) {
                toX = (float) (getWidth() - orgPixelMap.getImageInfo().size.width * quickScale);
            }
            if (getHeight() - orgPixelMap.getImageInfo().size.height * quickScale > toY) {
                toY = (float) (getHeight() - orgPixelMap.getImageInfo().size.height * quickScale);
            }
            if (0 < toX) {
                toX = 0;
            }
            if (0 < toY) {
                toY = 0;
            }
            if (getWidth() > orgPixelMap.getImageInfo().size.width * quickScale) {
                toX = (float) (getWidth() - orgPixelMap.getImageInfo().size.width * quickScale) / TWO;
            }
            if (getHeight() > orgPixelMap.getImageInfo().size.height * quickScale) {
                toY = (float) (getHeight() - orgPixelMap.getImageInfo().size.height * quickScale) / TWO;
            }

            startScaleAnimator(mratio, quickScale, offsetX, toX, offsetY, toY);
            quickScale = maxRatio;
        }
    }

    private void startScaleAnimator(
        double fromRatio,
        double toRatio,
        float... formTo) {
        float fromX = formTo[0];
        float toX = formTo[1];
        float fromY = formTo[TWO];
        float toY = formTo[THREE];

        if (scaleAnimatorValue == null) {
            scaleAnimatorValue = new AnimatorValue();
            scaleAnimatorValue.setCurveType(Animator.CurveType.ACCELERATE_DECELERATE);
        }
        scaleAnimatorValue.setDuration(DURATION_TIME);
        scaleAnimatorValue.setValueUpdateListener(
            (animatorValue, vv) -> {
                offsetX = fromX + (toX - fromX) * vv;
                offsetY = fromY + (toY - fromY) * vv;
                ratio = (float) (fromRatio + (toRatio - fromRatio) * vv);
                invalidate();
            });
        scaleAnimatorValue.setStateChangedListener(
            new Animator.StateChangedListener() {
                @Override
                public void onStart(Animator animator) {
                }

                @Override
                public void onStop(Animator animator) {
                }

                @Override
                public void onCancel(Animator animator) {
                }

                @Override
                public void onEnd(Animator animator) {
                    isScaleAnimator = false;
                    invalidate();
                }

                @Override
                public void onPause(Animator animator) {
                }

                @Override
                public void onResume(Animator animator) {
                }
            });
        isScaleAnimator = true;
        scaleAnimatorValue.start();
    }
}
